array-api-shape-check 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,34 @@
1
+ __version__ = "0.1.0"
2
+ from ._main import (
3
+ InconsistentNdimError,
4
+ InconsistentNdimErrorMultipleSolutions,
5
+ InconsistentNdimErrorNoSolutions,
6
+ InconsistentShapeError,
7
+ SubscriptInfoFromShape,
8
+ SubscriptInfoFromShapeItem,
9
+ SubscriptInfoFromShapeItemUnchecked,
10
+ SubscriptInfoFromShapeItemUnique,
11
+ SubscriptInfoFromSubcript,
12
+ SubscriptInfoFromSubcriptItem,
13
+ _parse_shapes,
14
+ check_shapes,
15
+ parse_subscripts,
16
+ parse_variable_ndim,
17
+ )
18
+
19
+ __all__ = [
20
+ "InconsistentNdimError",
21
+ "InconsistentNdimErrorMultipleSolutions",
22
+ "InconsistentNdimErrorNoSolutions",
23
+ "InconsistentShapeError",
24
+ "SubscriptInfoFromShape",
25
+ "SubscriptInfoFromShapeItem",
26
+ "SubscriptInfoFromShapeItemUnchecked",
27
+ "SubscriptInfoFromShapeItemUnique",
28
+ "SubscriptInfoFromSubcript",
29
+ "SubscriptInfoFromSubcriptItem",
30
+ "_parse_shapes",
31
+ "check_shapes",
32
+ "parse_subscripts",
33
+ "parse_variable_ndim",
34
+ ]
@@ -0,0 +1,446 @@
1
+ import re
2
+ from collections import defaultdict
3
+ from collections.abc import Sequence
4
+ from itertools import groupby
5
+ from typing import Any
6
+
7
+ import attrs
8
+ import numpy as np
9
+ from array_api.latest import Array
10
+
11
+
12
+ @attrs.frozen(kw_only=True)
13
+ class SubscriptInfoFromSubcriptItem:
14
+ name: str
15
+ """The name of the subscript, must be of length 1 and must not be '*' or '.'"""
16
+ is_variable: bool
17
+ """Whether the subscript is variable (* prefix or ...)"""
18
+
19
+ def __repr__(self) -> str:
20
+ return f"{'*' if self.is_variable else ''}{self.name}"
21
+
22
+
23
+ @attrs.frozen(kw_only=True)
24
+ class SubscriptInfoFromSubcript:
25
+ all: tuple[tuple[SubscriptInfoFromSubcriptItem, ...], ...]
26
+ unique: tuple[SubscriptInfoFromSubcriptItem, ...]
27
+
28
+
29
+ @attrs.frozen(kw_only=True)
30
+ class SubscriptInfoFromShapeItemUnchecked(SubscriptInfoFromSubcriptItem):
31
+ shape_current: tuple[int, ...]
32
+ """The shape of the operand corresponding to this subscript"""
33
+
34
+ def __repr__(self) -> str:
35
+ if self.is_variable:
36
+ return f"*{self.name}:{self.shape_current}"
37
+ else:
38
+ return f"{self.name}:{self.shape_current[0]}"
39
+
40
+
41
+ @attrs.frozen(kw_only=True)
42
+ class SubscriptInfoFromShapeItemUnique(SubscriptInfoFromSubcriptItem):
43
+ shape_broadcasted: tuple[int, ...]
44
+ """The shape after broadcasting with other subscripts with the same name"""
45
+
46
+ def __repr__(self) -> str:
47
+ if self.is_variable:
48
+ return f"*{self.name}:{self.shape_broadcasted}"
49
+ else:
50
+ return f"{self.name}:{self.shape_broadcasted[0]}"
51
+
52
+
53
+ @attrs.frozen(kw_only=True)
54
+ class SubscriptInfoFromShapeItem(SubscriptInfoFromShapeItemUnchecked):
55
+ shape_broadcasted: tuple[int, ...]
56
+ """The shape after broadcasting with other subscripts with the same name"""
57
+
58
+ def __repr__(self) -> str:
59
+ if self.is_variable:
60
+ if self.shape_current != self.shape_broadcasted:
61
+ return f"*{self.name}:{self.shape_current}->{self.shape_broadcasted}"
62
+ return f"*{self.name}:{self.shape_current}"
63
+ else:
64
+ if self.shape_current != self.shape_broadcasted:
65
+ return f"{self.name}:{self.shape_current[0]}->{self.shape_broadcasted[0]}"
66
+ return f"{self.name}:{self.shape_current[0]}"
67
+
68
+
69
+ @attrs.frozen(kw_only=True)
70
+ class SubscriptInfoFromShape:
71
+ all: tuple[tuple[SubscriptInfoFromShapeItem, ...], ...]
72
+ """The subscript info grouped by order, then by operand"""
73
+ unique: tuple[SubscriptInfoFromShapeItemUnique, ...]
74
+ """The unique subscripts,
75
+ ignoring order and shapes not broadcasted,
76
+ sorted lexicographically by name"""
77
+
78
+
79
+ class InconsistentNdimError(ValueError):
80
+ pass
81
+
82
+
83
+ class InconsistentNdimErrorMultipleSolutions(InconsistentNdimError):
84
+ def __init__(self, reason: str, *args: object) -> None:
85
+ super().__init__(
86
+ f"Inconsistent ndims: there are multiple possible solutions "
87
+ "to determine the number of dimensions "
88
+ f"for variable subscripts [{reason}]",
89
+ *args,
90
+ )
91
+
92
+
93
+ class InconsistentNdimErrorNoSolutions(InconsistentNdimError):
94
+ def __init__(self, reason: str, *args: object) -> None:
95
+ super().__init__(
96
+ f"Inconsistent ndims: there are no solution "
97
+ f"to determine the number of dimensions "
98
+ f"for variable subscripts [{reason}]",
99
+ *args,
100
+ )
101
+
102
+
103
+ class InconsistentShapeError(ValueError):
104
+ subscript_info: SubscriptInfoFromShape
105
+
106
+ def __init__(self, subscript_info: SubscriptInfoFromShape, reason: str, *args: object) -> None:
107
+ super().__init__("Inconsistent shapes: " + str(subscript_info.all) + f"\n {reason}", *args)
108
+ self.subscript_info = subscript_info
109
+
110
+
111
+ def _get_operand_names(*operands: Any) -> list[str | None]:
112
+ import inspect
113
+
114
+ frame = inspect.currentframe()
115
+ if frame is None:
116
+ return [None] * len(operands)
117
+ frame = frame.f_back
118
+ if frame is None:
119
+ return [None] * len(operands)
120
+ frame = frame.f_back
121
+ if frame is None:
122
+ return [None] * len(operands)
123
+ names = []
124
+ for operand in operands:
125
+ for var_name, var_value in frame.f_locals.items():
126
+ if var_value is operand:
127
+ names.append(var_name)
128
+ break
129
+ else:
130
+ names.append(None)
131
+ return names
132
+
133
+
134
+ def parse_subscripts(subscripts: str, /) -> SubscriptInfoFromSubcript:
135
+ """
136
+ Parse subscripts str.
137
+
138
+ Parameters
139
+ ----------
140
+ subscripts : str
141
+ Subscripts separated by "," per operand.
142
+
143
+ 1. Subscripts must be of length 1
144
+ 2. Subscripts must not be "*" or ".".
145
+ 3. If start with "*", the subscript is treated as variable.
146
+ 4. "..." is replaced with "*.".
147
+
148
+ Returns
149
+ -------
150
+ SubscriptInfoFromSubcript
151
+ The parsed subscript info.
152
+
153
+ Raises
154
+ ------
155
+ ValueError
156
+ If the subscript is invalid.
157
+
158
+ Examples
159
+ --------
160
+ >>> parse_subscripts("ij,*k*l,*li")
161
+ SubscriptInfoFromSubcript(all=((i, j), (*k, *l), (*l, i)), unique=(i, j, *k, *l))
162
+
163
+ """
164
+ # If . other than ...
165
+ if re.search(r"(?<!\.)\.(?!\.)", subscripts):
166
+ raise ValueError("Invalid subscript: '.' is not allowed except for '...'")
167
+ # Replace ... with *.
168
+ subscripts = re.sub(r"\.\.\.", "*.", subscripts)
169
+ subscripts = subscripts.rstrip()
170
+ info_all: tuple[tuple[SubscriptInfoFromSubcriptItem, ...], ...] = ()
171
+ for name_ in subscripts.split(","):
172
+ info_array: tuple[SubscriptInfoFromSubcriptItem, ...] = ()
173
+ is_variable = False
174
+ for name in name_:
175
+ if name == "*":
176
+ if is_variable:
177
+ raise ValueError("Invalid subscript: '*' cannot be repeated")
178
+ is_variable = True
179
+ else:
180
+ info_array += (SubscriptInfoFromSubcriptItem(name=name, is_variable=is_variable),)
181
+ is_variable = False
182
+ info_all += (info_array,)
183
+
184
+ info_unique: set[SubscriptInfoFromSubcriptItem] = {
185
+ x for info_array in info_all for x in info_array
186
+ }
187
+ # raise if there are subscripts with same name but different is_variable
188
+ names = set()
189
+ for info in info_unique:
190
+ if info.name in names:
191
+ raise ValueError(
192
+ f"Subscript '{info.name}' is duplicated with different variable status"
193
+ )
194
+ names.add(info.name)
195
+
196
+ return SubscriptInfoFromSubcript(
197
+ all=info_all, unique=tuple(sorted(info_unique, key=lambda x: x.name))
198
+ )
199
+
200
+
201
+ def parse_variable_ndim(subscripts: str, ndims: Sequence[int], /) -> dict[str, int]:
202
+ """
203
+ Parse variable subscript ndims by solving linear equations.
204
+
205
+ Parameters
206
+ ----------
207
+ subscripts : str
208
+ Subscripts separated by "," per operand.
209
+
210
+ 1. Subscripts must be of length 1
211
+ 2. Subscripts must not be "*" or ".".
212
+ 3. If start with "*", the subscript is treated as variable.
213
+ 4. "..." is replaced with "*.".]
214
+ ndims : Sequence[int]
215
+ The number of dimensions for each operand.
216
+
217
+ Returns
218
+ -------
219
+ SubscriptInfoFromSubcript
220
+ The parsed subscript info.
221
+
222
+ Raises
223
+ ------
224
+ ValueError
225
+ If the subscript is invalid.
226
+
227
+ Examples
228
+ --------
229
+ >>> parse_variable_ndim("ij,*k*l,*li", (2, 3, 3))
230
+ {'k': 1, 'l': 2}
231
+
232
+ Not enough information to determine variable subscript ndims:
233
+
234
+ >>> import pytest
235
+ >>> with pytest.raises(InconsistentNdimErrorMultipleSolutions, match="number of variables"):
236
+ ... parse_variable_ndim("*i*j", (2,))
237
+ >>> with pytest.raises(InconsistentNdimErrorMultipleSolutions, match="rank"):
238
+ ... parse_variable_ndim("*i*j,*i*j", (2, 2))
239
+
240
+ No solution to determine variable subscript ndims:
241
+
242
+ >>> with pytest.raises(InconsistentNdimErrorNoSolutions, match="residuals"):
243
+ ... parse_variable_ndim("*i,*i", (2, 3))
244
+ >>> with pytest.raises(InconsistentNdimErrorNoSolutions, match="negative"):
245
+ ... parse_variable_ndim("*ij", (0,))
246
+
247
+ """
248
+ info = parse_subscripts(subscripts)
249
+ del subscripts
250
+ if len(info.all) != len(ndims):
251
+ raise ValueError(
252
+ f"Number of subscripts ({len(info.all)}) does not match number of ndims ({len(ndims)})"
253
+ )
254
+
255
+ # decide dimensions by solving linear equations
256
+ info_variable_unique = [subscript for subscript in info.unique if subscript.is_variable]
257
+ if len(info_variable_unique) > len(info.all):
258
+ raise InconsistentNdimErrorMultipleSolutions("number of variables")
259
+ rhs = np.asarray(ndims, dtype=int)
260
+ del ndims
261
+ mat = np.zeros((len(info.all), len(info_variable_unique)), dtype=int)
262
+ for i, info_array in enumerate(info.all):
263
+ for subscript in info_array:
264
+ if subscript.is_variable:
265
+ j = info_variable_unique.index(subscript)
266
+ mat[i, j] += 1
267
+ else:
268
+ rhs[i] -= 1
269
+
270
+ # solve overdetermined linear equations using least squares method
271
+ variable_dims, residuals, rank, _singular_values = np.linalg.lstsq(mat, rhs, rcond=None)
272
+ if rank != len(info_variable_unique):
273
+ raise InconsistentNdimErrorMultipleSolutions("rank")
274
+ if residuals.size > 0 and not np.isclose(residuals[0], 0):
275
+ raise InconsistentNdimErrorNoSolutions("residuals")
276
+ variable_dims = np.round(variable_dims).astype(int)
277
+ if np.any(variable_dims < 0):
278
+ raise InconsistentNdimErrorNoSolutions("negative")
279
+ return {
280
+ subscript.name: int(variable_dims[j]) for j, subscript in enumerate(info_variable_unique)
281
+ }
282
+
283
+
284
+ def _parse_shapes(
285
+ subscripts: str, /, *operands: Array | tuple[int, ...]
286
+ ) -> tuple[tuple[SubscriptInfoFromShapeItemUnchecked, ...], ...]:
287
+ info = parse_subscripts(subscripts)
288
+ if len(info.all) != len(operands):
289
+ raise ValueError(
290
+ f"Number of subscripts ({len(info.all)}) "
291
+ f"does not match number of operands ({len(operands)})"
292
+ )
293
+ shapes: tuple[tuple[int, ...], ...] = ()
294
+ for operand in operands:
295
+ shape: tuple[int, ...] # type: ignore
296
+ if isinstance(operand, tuple):
297
+ shape = operand
298
+ elif hasattr(operand, "shape"):
299
+ if None in operand.shape:
300
+ raise ValueError("Operand shape cannot contain None")
301
+ shape = operand.shape
302
+ else:
303
+ raise TypeError(
304
+ f"Invalid operand: expected an array or a shape tuple, but got {type(operand)}"
305
+ )
306
+ shapes += (shape,) # type: ignore
307
+ del operands
308
+ ndims = [len(shape) for shape in shapes]
309
+ name_to_ndim = defaultdict(lambda: 1, parse_variable_ndim(subscripts, ndims))
310
+ del subscripts, ndims
311
+ info_all: tuple[tuple[SubscriptInfoFromShapeItemUnchecked, ...], ...] = ()
312
+ for info_array, shape in zip(info.all, shapes, strict=False):
313
+ info_array_new = ()
314
+ for item in info_array:
315
+ info_array_new += (
316
+ SubscriptInfoFromShapeItemUnchecked(
317
+ name=item.name,
318
+ is_variable=item.is_variable,
319
+ shape_current=shape[: name_to_ndim[item.name]],
320
+ ),
321
+ )
322
+ shape = shape[name_to_ndim[item.name] :]
323
+ info_all += (info_array_new,)
324
+ return info_all
325
+
326
+
327
+ def check_shapes(
328
+ subscripts: str, /, *operands: Array | tuple[int, ...], names: str | None = None
329
+ ) -> SubscriptInfoFromShape:
330
+ """
331
+ Parse variable subscript ndims by solving linear equations.
332
+
333
+ Parameters
334
+ ----------
335
+ subscripts : str
336
+ Subscripts separated by "," per operand.
337
+
338
+ 1. Subscripts must be of length 1
339
+ 2. Subscripts must not be "*" or ".".
340
+ 3. If start with "*", the subscript is treated as variable.
341
+ 4. "..." is replaced with "*.".]
342
+ operands : Array or tuple[int, ...]
343
+ Arrays or shape tuples corresponding to check.
344
+ ndims : Sequence[int]
345
+ The number of dimensions for each operand.
346
+ names : str | None
347
+ The names of operands separated by ",",
348
+ used for error messages. If None, operand indices are used instead.
349
+
350
+ Returns
351
+ -------
352
+ SubscriptInfoFromSubcript
353
+ The parsed subscript info.
354
+
355
+ Raises
356
+ ------
357
+ ValueError
358
+ If the subscript is invalid.
359
+
360
+ Examples
361
+ --------
362
+ >>> info = check_shapes("ij,*k*l,*li", (1, 4), (5, 6, 7), (1, 7, 3))
363
+ >>> info.all
364
+ ((i:1->3, j:4), (*k:(5,), *l:(6, 7)), (*l:(1, 7)->(6, 7), i:3))
365
+ >>> info.unique
366
+ (i:3, j:4, *k:(5,), *l:(6, 7))
367
+
368
+ Not enough information to determine variable subscript ndims:
369
+
370
+ >>> import pytest
371
+ >>> with pytest.raises(InconsistentNdimErrorMultipleSolutions, match="number of variables"):
372
+ ... check_shapes(
373
+ ... "*i*j",
374
+ ... (
375
+ ... 1,
376
+ ... 1,
377
+ ... ),
378
+ ... )
379
+ >>> with pytest.raises(InconsistentNdimErrorMultipleSolutions, match="rank"):
380
+ ... check_shapes("*i*j,*i*j", (1, 1), (1, 1))
381
+
382
+ No solution to determine variable subscript ndims:
383
+
384
+ >>> with pytest.raises(InconsistentNdimErrorNoSolutions, match="residuals"):
385
+ ... check_shapes("*i,*i", (1, 1), (1, 1, 1))
386
+ >>> with pytest.raises(InconsistentNdimErrorNoSolutions, match="negative"):
387
+ ... check_shapes("*ij", ())
388
+
389
+ Does not match:
390
+ >>> with pytest.raises(InconsistentShapeError):
391
+ ... check_shapes("ij,*k*l,*li", (3, 4), (5, 6), (1, 7, 3))
392
+
393
+ """
394
+ info_all = _parse_shapes(subscripts, *operands)
395
+ if names is not None:
396
+ names_ = [str(name) for name in names.split(",")]
397
+ if len(names_) != len(info_all):
398
+ raise ValueError(
399
+ f"Number of names ({len(names_)}) "
400
+ f"does not match number of operands ({len(info_all)})"
401
+ )
402
+ else:
403
+ names_ = [str(i) for i in range(len(info_all))]
404
+ info_flatten_keyed = [(i, item) for i, info_array in enumerate(info_all) for item in info_array]
405
+ errors = []
406
+ shape_broadcasted = {}
407
+ for key, group in groupby(info_flatten_keyed, key=lambda x: x[1].name):
408
+ group_list = list(group)
409
+ shapes = [item.shape_current for _, item in group_list]
410
+ try:
411
+ shape_broadcasted[key] = np.broadcast_shapes(*shapes)
412
+ except ValueError:
413
+ inner_text = ", ".join([f"{shape} ({names_[i]})" for i, shape in group_list])
414
+ errors.append(f"Key {key}: shapes {inner_text} are not broadcastable")
415
+
416
+ info_all_new = ()
417
+ for info_array in info_all:
418
+ info_array_new = ()
419
+ for item in info_array:
420
+ info_array_new += (
421
+ SubscriptInfoFromShapeItem(
422
+ name=item.name,
423
+ is_variable=item.is_variable,
424
+ shape_current=item.shape_current,
425
+ shape_broadcasted=shape_broadcasted.get(item.name), # type: ignore
426
+ ),
427
+ )
428
+ info_all_new += (info_array_new,)
429
+
430
+ info_unique = {
431
+ SubscriptInfoFromShapeItemUnique(
432
+ name=item.name,
433
+ is_variable=item.is_variable,
434
+ shape_broadcasted=shape_broadcasted.get(item.name), # type: ignore
435
+ )
436
+ for info_array in info_all_new
437
+ for item in info_array
438
+ }
439
+ result = SubscriptInfoFromShape(
440
+ all=info_all_new, unique=tuple(sorted(info_unique, key=lambda x: x.name))
441
+ )
442
+
443
+ if errors:
444
+ raise InconsistentShapeError(result, "\n".join(errors))
445
+
446
+ return result
File without changes
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: array-api-shape-check
3
+ Version: 0.1.0
4
+ Summary: Check shapes of input arrays easily
5
+ Author-email: 34j <34j.95a2p@simplelogin.com>
6
+ License-Expression: MIT
7
+ Project-URL: Bug Tracker, https://github.com/34j/array-api-shape-check/issues
8
+ Project-URL: Changelog, https://github.com/34j/array-api-shape-check/blob/main/CHANGELOG.md
9
+ Project-URL: documentation, https://array-api-shape-check.readthedocs.io
10
+ Project-URL: repository, https://github.com/34j/array-api-shape-check
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Requires-Python: >=3.12
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: attrs>=26.1
24
+ Requires-Dist: more-itertools>=11.0.2
25
+ Requires-Dist: numpy>=2.4.4
26
+ Requires-Dist: types-array-api>=1.1.4
27
+ Dynamic: license-file
28
+
29
+ # array-api-shape-check
30
+
31
+ <p align="center">
32
+ <a href="https://github.com/34j/array-api-shape-check/actions/workflows/ci.yml?query=branch%3Amain">
33
+ <img src="https://img.shields.io/github/actions/workflow/status/34j/array-api-shape-check/ci.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
34
+ </a>
35
+ <a href="https://array-api-shape-check.readthedocs.io">
36
+ <img src="https://img.shields.io/readthedocs/array-api-shape-check.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
37
+ </a>
38
+ <a href="https://codecov.io/gh/34j/array-api-shape-check">
39
+ <img src="https://img.shields.io/codecov/c/github/34j/array-api-shape-check.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
40
+ </a>
41
+ </p>
42
+ <p align="center">
43
+ <a href="https://github.com/astral-sh/uv">
44
+ <img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv">
45
+ </a>
46
+ <a href="https://github.com/astral-sh/ruff">
47
+ <img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
48
+ </a>
49
+ <a href="https://github.com/j178/prek">
50
+ <img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/j178/prek/master/docs/assets/badge-v0.json" alt="prek">
51
+ </a>
52
+ </p>
53
+ <p align="center">
54
+ <a href="https://pypi.org/project/array-api-shape-check/">
55
+ <img src="https://img.shields.io/pypi/v/array-api-shape-check.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
56
+ </a>
57
+ <img src="https://img.shields.io/pypi/pyversions/array-api-shape-check.svg?style=flat-square&logo=python&amp;logoColor=fff" alt="Supported Python versions">
58
+ <img src="https://img.shields.io/pypi/l/array-api-shape-check.svg?style=flat-square" alt="License">
59
+ </p>
60
+
61
+ ---
62
+
63
+ **Documentation**: <a href="https://array-api-shape-check.readthedocs.io" target="_blank">https://array-api-shape-check.readthedocs.io </a>
64
+
65
+ **Source Code**: <a href="https://github.com/34j/array-api-shape-check" target="_blank">https://github.com/34j/array-api-shape-check </a>
66
+
67
+ ---
68
+
69
+ Check shapes of input arrays easily.
70
+
71
+ ## Installation
72
+
73
+ Install this via pip (or your favourite package manager):
74
+
75
+ `pip install array-api-shape-check`
76
+
77
+ ## Usage
78
+
79
+ ```python
80
+ >>> from array_api_shape_check import check_shapes
81
+ >>> info = check_shapes("ij,*k*l,*li", (1, 4), (5, 6, 7), (1, 7, 3))
82
+ >>> info.all
83
+ ((i:1->3, j:4), (*k:(5,), *l:(6, 7)), (*l:(1, 7)->(6, 7), i:3))
84
+ >>> info.unique
85
+ (i:3, j:4, *k:(5,), *l:(6, 7))
86
+ ```
87
+
88
+ Internally `check_shapes()` calls `parse_variable_ndim()`, which determines the number of dimensions for variable subscripts by least squares. If this is successful, checks if each subscript is consistent, then finnaly raises error for all inconsistencies at once.
89
+
90
+ Diving into the details of the first item:
91
+
92
+ ```python
93
+ >>> item = info.all[0][0]
94
+ >>> item.name # the name of the subscript
95
+ 'i'
96
+ >>> item.is_variable # whether the subscript is variable (starts with "*")
97
+ False
98
+ >>> item.shape_current # the current shape of the subscript
99
+ (1,)
100
+ >>> item.shape_broadcasted # the broadcasted shape of the subscript
101
+ (3,)
102
+ ```
103
+
104
+ Not enough information to determine variable subscript ndims:
105
+
106
+ ```python
107
+ >>> import pytest
108
+ >>> from array_api_shape_check import InconsistentNdimErrorMultipleSolutions, InconsistentNdimErrorNoSolutions, InconsistentShapeError
109
+ >>> with pytest.raises(InconsistentNdimErrorMultipleSolutions, match="number of variables"):
110
+ ... check_shapes(
111
+ ... "*i*j",
112
+ ... (
113
+ ... 1,
114
+ ... 1,
115
+ ... ),
116
+ ... )
117
+ >>> with pytest.raises(InconsistentNdimErrorMultipleSolutions, match="rank"):
118
+ ... check_shapes("*i*j,*i*j", (1, 1), (1, 1))
119
+ ```
120
+
121
+ No solution to determine variable subscript ndims:
122
+
123
+ ```python
124
+ >>> with pytest.raises(InconsistentNdimErrorNoSolutions, match="residuals"):
125
+ ... check_shapes("*i,*i", (1, 1), (1, 1, 1))
126
+ >>> with pytest.raises(InconsistentNdimErrorNoSolutions, match="negative"):
127
+ ... check_shapes("*ij", ())
128
+ ```
129
+
130
+ Does not match:
131
+
132
+ ```python
133
+ >>> with pytest.raises(InconsistentShapeError):
134
+ ... check_shapes("ij,*k*l,*li", (3, 4), (5, 6), (1, 7, 3))
135
+ ```
136
+
137
+ ## Contributors ✨
138
+
139
+ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
140
+
141
+ <!-- prettier-ignore-start -->
142
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
143
+ <!-- markdownlint-disable -->
144
+ <!-- markdownlint-enable -->
145
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
146
+ <!-- prettier-ignore-end -->
147
+
148
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
149
+
150
+ ## Credits
151
+
152
+ [![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-orange.json)](https://github.com/copier-org/copier)
153
+
154
+ This package was created with
155
+ [Copier](https://copier.readthedocs.io/) and the
156
+ [browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
157
+ project template.
@@ -0,0 +1,8 @@
1
+ array_api_shape_check/__init__.py,sha256=Nr4JGOF0JGdFAgQ84lyiQMcW0k1i1DYvVVusW_d_T-k,940
2
+ array_api_shape_check/_main.py,sha256=ebEBTb8o3yG19d0F9yfMB44dMLxJVJWt9KuH7e6IYV0,15391
3
+ array_api_shape_check/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ array_api_shape_check-0.1.0.dist-info/licenses/LICENSE,sha256=Otwb96xcK4QBqXXVdBKpLZVnc4mJuFTa8rsMUEpphMM,1061
5
+ array_api_shape_check-0.1.0.dist-info/METADATA,sha256=X1F3n9ScdYFEcftDb5LpXrNA1wefvX8X7RysP2iO7vg,6275
6
+ array_api_shape_check-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ array_api_shape_check-0.1.0.dist-info/top_level.txt,sha256=pirw2DeaWnDU_rSg3CZ1MsE-tJWEvndYSQG0illDMkI,22
8
+ array_api_shape_check-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,22 @@
1
+
2
+ MIT License
3
+
4
+ Copyright (c) 2025 34j
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ array_api_shape_check